﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ShredTheLog.Common;
using System.Globalization;
using System.Runtime.Serialization.Json;
using System.IO;
using System.Runtime.Serialization;
using System.Diagnostics.Contracts;
using System.ComponentModel.Composition;
using ShredTheLog.Api.DataContracts;
using ShredTheLog.Api;
using System.Diagnostics;
using System.Net;

namespace ShredTheLog.Translation
{
    [Export(typeof(ITranslator))]
    [OrderMetadata(Order = 0)]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public sealed class GoogleTranslator : ITranslator
    {
        public const string IDString = "Google";

        public string ID { get { return IDString; } }
        public string Name { get { return "Google Translate"; } }

        private const string ApiVersion = "1.0";
        private const string ApiKey = "ABQIAAAAFuLC_G_mfMS8cRBiPHJi9BRSpUqEK6sa2M0B_xU0czn8L_IsIRRcY_2HktIiSt8RfEaTXdXBocfcZg";
        private const string BaseUriString = "http://ajax.googleapis.com/ajax/services/language/";
        private static readonly string TranslateUriString = String.Format(CultureInfo.InvariantCulture, "{0}translate?v={1}&key={2}&", BaseUriString, ApiVersion, ApiKey);
        private static readonly string DetectUriString = String.Format(CultureInfo.InvariantCulture, "{0}detect?v={1}&key={2}&", BaseUriString, ApiVersion, ApiKey);
        private const int StatusOk = 200;

        private static readonly Dictionary<Language, string> LanguageCodes = new Dictionary<Language, string>()
        {
            { Language.Albanian, "sq" },
            { Language.English, "en" },
            { Language.Arabic, "ar" },
            { Language.Afrikaans, "af" },
            { Language.Belorussian, "be" },
            { Language.Bulgarian, "bg" },
            { Language.Welsh, "cy" },
            { Language.Hungarian, "hu" },
            { Language.Vietnamese, "vi" },
            { Language.Galician, "gl" },
            { Language.Dutch, "nl" },
            { Language.Greek, "el" },
            { Language.Danish, "da" },
            { Language.Hebrew, "iw" },
            { Language.Yiddish, "yi" },
            { Language.Indonesian, "id" },
            { Language.Irish, "ga" },
            { Language.Icelandic, "is" },
            { Language.Spanish, "es" },
            { Language.Italian, "it" },
            { Language.Catalan, "ca" },
            { Language.ChineseOther, "zh-TW" },
            { Language.ChineseMandarin, "zh-CN" },
            { Language.Korean, "ko" },
            { Language.Latvian, "lv" },
            { Language.Lithuanian, "lt" },
            { Language.Macedonian, "mk" },
            { Language.Malay, "ms" },
            { Language.Maltese, "mt" },
            { Language.German, "de" },
            { Language.Norwegian, "no" },
            { Language.PersianFarsi, "fa" },
            { Language.Polish, "pl" },
            { Language.Portuguese, "pt" },
            { Language.Romanian, "ro" },
            { Language.Russian, "ru" },
            { Language.Serbian, "sr" },
            { Language.Slovak, "sk" },
            { Language.Slovenian, "sl" },
            { Language.Swahili, "sw" },
            { Language.Tagalog, "tl" },
            { Language.Thai, "th" },
            { Language.Turkish, "tr" },
            { Language.Ukrainian, "uk" },
            { Language.Finnish, "fi" },
            { Language.French, "fr" },
            { Language.Hindi, "hi" },
            { Language.Croatian, "hr" },
            { Language.Czech, "cs" },
            { Language.Swedish, "sv" },
            { Language.Estonian, "et" },
            { Language.Japanese, "ja" },
        };

        private static readonly DataContractJsonSerializer TranslateResultJsonSerializer = new DataContractJsonSerializer(typeof(TranslateResult));
        private static readonly DataContractJsonSerializer DetectResultJsonSerializer = new DataContractJsonSerializer(typeof(DetectResult));

        private Language GetLanguage(string languageCode)
        {
            Contract.Requires(!String.IsNullOrWhiteSpace(languageCode));

            var result = LanguageCodes.SingleOrDefault(i => i.Value == languageCode);
            if (result.Value != null)
            {
                return result.Key;
            }
            else
            {
                Trace.WriteLine(String.Format("{0}: Language code {1} was not recognized.", this.GetType().Name, languageCode));
                return Language.English;
            }
        }

        public IObservable<TranslateResponse> Translate(TranslateRequest translateRequest, bool ignoreErrors)
        {
            if (String.IsNullOrWhiteSpace(translateRequest.Text) || (translateRequest.SourceLanguage.HasValue && translateRequest.SourceLanguage.Value == translateRequest.TargetLanguage))
            {
                var result1 = Observable.Return(new TranslateResponse(
                    translateRequest.Text,
                    translateRequest.SourceLanguage.HasValue ? translateRequest.SourceLanguage.Value : Language.English,
                    translateRequest.Context));
                Contract.Assume(result1 != null);
                return result1;
            }

            string uriParams = String.Format(CultureInfo.InvariantCulture, "q={0}&langpair={1}|{2}",
                Uri.EscapeDataString(translateRequest.Text),
                translateRequest.SourceLanguage.HasValue ? LanguageCodes[translateRequest.SourceLanguage.Value] : String.Empty,
                LanguageCodes[translateRequest.TargetLanguage]);

            var uri = new Uri(TranslateUriString + uriParams);
            if (uri.AbsoluteUri.Length > 2000)
            {
                string message = String.Format("String was too long for this translator: \"{0}\"", translateRequest.Text);
                if (ignoreErrors)
                {
                    Trace.WriteLine(message);
                    return Observable.Empty<TranslateResponse>();
                }
                else
                {
                    return Observable.Throw<TranslateResponse>(new ApplicationException(message));
                }
            }

            var result2 =
                (from jsonResponseString in WebHelper.GetHttpRequest(uri, Encoding.UTF8)
                 let response = (TranslateResult)TranslateResultJsonSerializer.ReadObject(jsonResponseString, false)
                 select response)
                .Do(
                response =>
                {
                    if (response == null)
                    {
                        string message = String.Format("Failed to translate \"{0}\".", translateRequest.Text);
                        if (ignoreErrors)
                            Trace.WriteLine(message);
                        else
                            throw new ApplicationException(message);
                    }
                    else if (response.ResponseStatus != StatusOk)
                    {
                        if (ignoreErrors)
                        {
                            Trace.WriteLine(String.Format(
                                "{0}: Failed to translate «{1}» from {2} to {3} (error {4:d}: {5}).",
                                this.GetType().Name,
                                translateRequest.Text ?? String.Empty,
                                translateRequest.SourceLanguage.HasValue ? translateRequest.SourceLanguage.ToString() : "Auto",
                                translateRequest.TargetLanguage.ToString(),
                                response.ResponseStatus,
                                response.ResponseDetails ?? "Unknown error"));
                        }
                        else
                        {
                            throw new GoogleTranslatorException(response);
                        }
                    }
                })
                .Where(response => response != null && response.ResponseStatus == StatusOk && response.ResponseData != null)
                .Select(response => new TranslateResponse(
                    WebUtility.HtmlDecode(response.ResponseData.TranslatedText),
                    translateRequest.SourceLanguage.HasValue ? translateRequest.SourceLanguage.Value : GetLanguage(response.ResponseData.DetectedSourceLanguage),
                    translateRequest.Context));
            Contract.Assume(result2 != null);
            return result2;
        }

        public IObservable<DetectResponse> Detect(string text)
        {
            if (String.IsNullOrWhiteSpace(text))
            {
                var result1 = Observable.Return(new GoogleDetectResponse(Language.English, false, 0));
                Contract.Assume(result1 != null);
                return result1;
            }

            string uriString = String.Format(CultureInfo.InvariantCulture, "{0}q={1}", DetectUriString, Uri.EscapeDataString(text));

            var uri = new Uri(uriString);
            if (uri.AbsoluteUri.Length > 2000)
            {
                return Observable.Throw<DetectResponse>(new ApplicationException(String.Format("String was too long for this translator: \"{0}\"", text)));
            }

            var result2 =
                (from jsonResponseString in WebHelper.GetHttpRequest(uri, Encoding.UTF8)
                 let response = (DetectResult)DetectResultJsonSerializer.ReadObject(jsonResponseString, false)
                 select response)                
                .Do(
                response =>
                    {
                        if (response == null)
                            throw new InvalidOperationException(String.Format("Failed to detect language for \"{0}\".", text));

                        if (response.ResponseStatus != StatusOk)
                            throw new GoogleTranslatorException(response);
                    })
                .Where(response => response!= null && response.ResponseStatus == StatusOk && response.ResponseData != null)
                .Select(response =>new GoogleDetectResponse(GetLanguage(response.ResponseData.Language), response.ResponseData.IsReliable, response.ResponseData.Confidence));
            Contract.Assume(result2 != null);
            return result2;
        }

        public IEnumerable<Language> SupportedLanguages { get { return LanguageCodes.Keys; } }

        public override string ToString()
        {
            return Name;
        }

        public sealed class GoogleTranslatorException : ApplicationException
        {
            public int Status { get; private set; }
            public string Details { get; private set; }

            internal GoogleTranslatorException(GoogleResult googleResult) 
                : base(String.Format(CultureInfo.CurrentCulture, "Error {0:d}: {1}", googleResult.ResponseStatus, googleResult.ResponseDetails ?? String.Empty))
            {
                Contract.Requires(googleResult != null);

                Status = googleResult.ResponseStatus;
                Details = googleResult.ResponseDetails;
            }

            public static readonly GoogleTranslatorException[] Empty = new GoogleTranslatorException[0];
        }

        [DataContract]
        internal abstract class GoogleResult
        {
            [DataMember(Name = "responseDetails")]
            public string ResponseDetails { get; set; }
            [DataMember(Name = "responseStatus")]
            public int ResponseStatus { get; set; }
        }

        [DataContract]
        sealed class TranslateResult : GoogleResult
        {
            [DataMember(Name = "responseData")]
            public Data ResponseData { get; set; }

            [DataContract]
            public sealed class Data
            {
                [DataMember(Name = "translatedText")]
                public string TranslatedText { get; set; }
                [DataMember(Name = "detectedSourceLanguage")]
                public string DetectedSourceLanguage { get; set; }
            }
        }

        [DataContract]
        sealed class DetectResult : GoogleResult
        {
            [DataMember(Name = "responseData")]
            public Data ResponseData { get; set; }

            [DataContract]
            public sealed class Data
            {
                [DataMember(Name = "language")]
                public string Language { get; set; }
                [DataMember(Name = "isReliable")]
                public bool IsReliable { get; set; }
                [DataMember(Name = "confidence")]
                public double Confidence { get; set; }
            }
        }
    }

    public sealed class GoogleDetectResponse : DetectResponse
    {
        public bool IsReliable { get; private set; }
        public double Confidence { get; private set; }

        public GoogleDetectResponse(Language detectedLanguage, bool isReliable, double confidence)
            : base(detectedLanguage)
        {
            IsReliable = isReliable;
            Confidence = confidence;
        }

        public override string ToString()
        {
            return String.Format(CultureInfo.CurrentCulture, "{0; {2} = {3}; {4} = {5:f}",
                base.ToString(),
                Helper.GetPropertyName(() => IsReliable),
                IsReliable.ToString(CultureInfo.InvariantCulture),
                Helper.GetPropertyName(() => Confidence),
                Confidence);
        }
    }
}